﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using sdd.subsystems.audio;
using sdd.subsystems.memory;
using sdd.subsystems.display;
using sdd.subsystems.ui;
using sdd.subsystems.ui.screens;

// NISTON StreamOne - Streaming Audio Tuner App for SDD#
// This App implements a Streaming Audio Tuner Device on the SDD# core
// (c) 2014-2015 Chris Burri. MIT License applies. 
// See http://niston.wordpress.com

namespace sdd.apps
{
    public class Tuner : IApplication
    {
        private System.Timers.Timer UIUpdateTimer = new System.Timers.Timer();
        private int _CurrentStationId = 0;
        

        private MenuItem mnuTuner;
        private SwitchMenuItem optScanOnStreamFailure;
        private SwitchMenuItem optReconnectOnBreak;
        private IntMenuItem optReconnectMaxAttempts;

        public string Name { get { return "Stream One"; } }
        
        public Director Director { set; get; }

        public AudioStreamer Streamer { private set; get; }

        public ScrTuner TunerScreen { private set; get; }     

        public MenuItem AppMenu { get; private set; }

        public ScrMenu AppMenuScreen { get; private set; }

        // station memory
        public ProgramMemory Memory { private set; get; }

        // the selected operational status
        public OperationalStatuses OperationalState { private set; get; }


        public Tuner()
        {
            Memory = new ProgramMemory();

            Streamer = new AudioStreamer(2, 2); // 44k1 Stereo, 2 streams max
            Streamer.MetadataUpdated += _Streamer_OnMetadataUpdated;
            Streamer.StreamChanging += _Streamer_OnStreamChanging;
            Streamer.StreamChanged += _Streamer_OnStreamChanged;
            Streamer.StreamEnded += _Streamer_OnStreamEnded;
        }

        ~Tuner()
        {
            Streamer.MetadataUpdated -= _Streamer_OnMetadataUpdated;
            Streamer.StreamChanging -= _Streamer_OnStreamChanging;
            Streamer.StreamChanged -= _Streamer_OnStreamChanged;
            Streamer.StreamEnded -= _Streamer_OnStreamEnded;
        }

        
        public void Initialize()
        {
            // add a bunch of default stations
            Memory.Add(1, "http://radiol.ice.infomaniak.ch/radiol.mp3");
            Memory.Add(2, "http://listen.streamonomy.com/radioswingworldwide");
            Memory.Add(3, "http://yp.shoutcast.com/sbin/tunein-station.pls?id=73236");
            Memory.Add(4, "http://yp.shoutcast.com/sbin/tunein-station.pls?id=181612");
            Memory.Add(5, "http://yp.shoutcast.com/sbin/tunein-station.pls?id=183330");
            Memory.Add(6, "http://yp.shoutcast.com/sbin/tunein-station.pls?id=338128");
            Memory.Add(7, "http://yp.shoutcast.com/sbin/tunein-station.pls?id=100621");
            Memory.Add(8, "http://www.radioswissjazz.ch/live/aacp.m3u");
            Memory.Add(9, "http://yp.shoutcast.com/sbin/tunein-station.pls?id=226429");
            Memory.Add(10, "http://yp.shoutcast.com/sbin/tunein-station.pls?id=102285");
            Memory.Add(11, "http://yp.shoutcast.com/sbin/tunein-station.pls?id=212505");
            Memory.Add(12, "http://www.guldkanalen.se/static/streamGK70/128MP3.pls");
            Memory.Add(13, "http://www.guldkanalen.se/static/streamGK80/128MP3.pls");
            Memory.Add(14, "http://stream-ice.mtgradio.com:8080/stat_bandit.m3u");
            Memory.Add(15, "http://scb2.fantis.se:8080/listen.pls");
            Memory.Add(16, "http://194.16.21.227/rock_se_mp3.m3u");
            Memory.Add(17, "http://194.16.21.227/vinyl_se_mp3.m3u");
            Memory.Add(18, "http://sverigesradio.se/topsy/direkt/132-hi-mp3.m3u");
            Memory.Add(19, "http://sverigesradio.se/topsy/direkt/179-hi-mp3.pls");
            Memory.Add(20, "http://dir.xiph.org/listen/2692553/listen.m3u");
            Memory.Add(21, "http://dir.xiph.org/listen/2670757/listen.m3u");
            Memory.Add(22, "http://dir.xiph.org/listen/2658852/listen.m3u");
            Memory.Add(23, "http://dir.xiph.org/listen/2403411/listen.m3u");
            Memory.Add(24, "http://dir.xiph.org/listen/1863614/listen.m3u");
            Memory.Add(25, "http://dir.xiph.org/listen/2658961/listen.m3u");
            Memory.Add(26, "http://dir.xiph.org/listen/2634273/listen.m3u");
            Memory.Add(27, "http://dir.xiph.org/listen/2682920/listen.m3u");
            Memory.Add(28, "http://yp.shoutcast.com/sbin/tunein-station.m3u?id=150854");
            Memory.Add(29, "http://yp.shoutcast.com/sbin/tunein-station.m3u?id=1267820");
            Memory.Add(30, "http://yp.shoutcast.com/sbin/tunein-station.m3u?id=114517");
            Memory.Add(31, "http://yp.shoutcast.com/sbin/tunein-station.m3u?id=361317");
            Memory.Add(32, "http://yp.shoutcast.com/sbin/tunein-station.m3u?id=198977");

            Memory.Add(98, "http://mp3.radio.ch/radiozuerisee128k.m3u");
            Memory.Add(99, "http://mp3.radio.ch/radiobern164k.m3u");
            
            Director.InternetAvailability.BeginTest += InternetAvailability_BeginTest;

            AppMenu = new MenuItem("streamone.appmenu", "Stream One");
            Director.ScreenManager.Menus.Register(AppMenu);

            TunerScreen = new ScrTuner(this);
            AppMenuScreen = new ScrMenu(AppMenu.Caption, AppMenu.Caption, AppMenu);
            Director.ScreenManager.Register(TunerScreen);
            Director.ScreenManager.Register(AppMenuScreen);



            

            // build Tuner settings menu
            mnuTuner = new MenuItem("streamone.appmenu.nonstopstreaming", "NonStop Streaming");
            // register menu
            Director.ScreenManager.Menus.Register(mnuTuner, AppMenu);
            // menu items
            optReconnectOnBreak = new SwitchMenuItem("streamone.appmenu.nonstopstreaming", "Recon dead", true);
            optReconnectMaxAttempts = new IntMenuItem("app.menu.StreamOne.tuner_settings.reconnect_max_tries", "Recon max attempts", 0, 0, 256);
            optScanOnStreamFailure = new SwitchMenuItem("app.menu.StreamOne.tuner_settings.scan_on_recon_fail", "Scan on recon fail", false);

            // override displayed value
            optReconnectMaxAttempts.Formatting += OptReconnectMaxAttempts_Formatting;

            // register items
            mnuTuner.SubItems.Add(optReconnectOnBreak);
            mnuTuner.SubItems.Add(optReconnectMaxAttempts);
            mnuTuner.SubItems.Add(optScanOnStreamFailure);
            


            UIUpdateTimer.Elapsed += UIUpdateTimer_OnElapsed;
            UIUpdateTimer.Interval = 100;
            UIUpdateTimer.Start();

        }

        private void InternetAvailability_BeginTest(object sender, subsystems.network.InternetAvailabilityTest.BeginTestEventArgs e)
        {
            // see if Streamer has a stream that is playing
            if (Streamer.CurrentStream != null)
            {
                // assume we have internet connection if current stream is playing
                e.IsConnected = (Streamer.CurrentStream.Status == InetStream.PlayerStatuses.Playing);
            }
        }




        private void OptReconnectMaxAttempts_Formatting(object sender, MenuItemFormattingEventArgs e)
        {
            // display 0 (zero) value as "Unlimited"
            if ((int)e.Value == 0)
            {
                e.FormattedValue = "No Limit";
            }
        }

        public void Terminate()
        {
            Director.InternetAvailability.BeginTest -= InternetAvailability_BeginTest;
            Director.ScreenManager.UnRegister(TunerScreen);
        }

        public void Activate()
        {
            // select station
            TunerScreen.ProgramID = 1;
            CurrentStationId = Memory.First.Id;
            Director.ScreenManager.Show(TunerScreen);
            // UGLY ASS HACK TO WORK AROUND MYSTERIOUS, FUCKING UNREASONABLE, OUT-OF-NOWHERE BUG
            //Play();
            //Stop();
            // start selected station
            Play();
        }

        public void Deactivate()
        {
            // TODO: don't stop
            Stop();
        }
        
        
        // current station Id (number)
        public int CurrentStationId
        {
            set
            {
                if (_CurrentStationId != value)
                {
                    _CurrentStationId = value;
                    
                    TunerScreen.ProgramID = _CurrentStationId;
                }
            }
            get
            {
                return _CurrentStationId;
            }
        }

        public ScrMenu ScreenAppMenu
        {
            get { return AppMenuScreen; }
        }

        public IScreen ScreenMain
        {
            get { return TunerScreen; }
        }




        // go to next available station
        public bool Next()
        {
            Station next = Memory.GetNext(CurrentStationId);
            int tryStationId = CurrentStationId;
            if (next != null)
            {
                switch (OperationalState)
                {
                    case OperationalStatuses.Play:
                        OperationalState = OperationalStatuses.Next;
                        while (Streamer.Start(next.Url, true) == null & OperationalState == OperationalStatuses.Next)
                        {
                            next = Memory.GetNext(next.Id);
                            System.Threading.Thread.Sleep(20);
                        }
                        return true;
                    case OperationalStatuses.Stop:
                        TunerScreen.StreamFormat = "";
                        TunerScreen.ArtistAndTitle = "";
                        CurrentStationId = next.Id;
                        return true;
                }
            }
            return false;
        }

        // go to previous available station
        public bool Previous()
        {
            Station prev = Memory.GetPrevious(CurrentStationId);
            if (prev != null)
            {
                switch (OperationalState)
                {
                    case OperationalStatuses.Play:
                        OperationalState = OperationalStatuses.Previous;
                        while (Streamer.Start(prev.Url, true) == null & OperationalState == OperationalStatuses.Previous)
                        {
                            prev = Memory.GetPrevious(prev.Id);
                            System.Threading.Thread.Sleep(20);
                        }
                        return true;
                    case OperationalStatuses.Stop:
                        TunerScreen.StreamFormat = "";
                        TunerScreen.ArtistAndTitle = "";
                        CurrentStationId = prev.Id;
                        return true;
                }
            }
            return false;
        }

        // start playback of current station
        public bool Play()
        {
            OperationalState = OperationalStatuses.Play;
            Station curStation = Memory.GetStation(CurrentStationId);
            TunerScreen.ProgramID = CurrentStationId;
            try
            {
                TunerScreen.ArtistAndTitle = "Connecting...";
                TunerScreen.StreamFormat = "";
                TunerScreen.StationName = "";
                Streamer.Start(Memory.GetStation(CurrentStationId).Url);
                Memory.GetNext(CurrentStationId).ConnectionRetryCount = 0;  // connected, so reset the retry counter
                // EXPERIMENTAL
                Director.InternetAvailability.ReportConnectivity();
                return true;
            }
            catch (BASSException ex)
            {
                Stop();
                TunerScreen.ArtistAndTitle = "Station unavailable.";
                TunerScreen.StreamFormat = string.Format("{0} ERROR.", ex.BassErrorText.Replace("BASS_ERROR_", ""));
                // EXPERIMENTAL
                Director.InternetAvailability.ReportConnectivityProblem();
                return false;
            }
        }

        // stop all playback
        public bool Stop()
        {
            OperationalState = OperationalStatuses.Stop;
            Streamer.StopAll(true);
            if (TunerScreen != null)
            {
                TunerScreen.ArtistAndTitle = "Stopped.";
                TunerScreen.StationName = "";
                TunerScreen.StreamFormat = "";
                TunerScreen.ProgramID = _CurrentStationId;
                TunerScreen.BlinkProgramId = false;
            }
            return true;
        }



        // AudioSteamer events
        void _Streamer_OnMetadataUpdated(object sender, EventArgs e)
        {
            AudioStreamer streamer = (AudioStreamer)sender;
            InetStream stream = streamer.CurrentStream;
            TunerScreen.ArtistAndTitle = streamer.FormattedSongLine;
            TunerScreen.StationName = stream.Station;
            //string streamFormat = ((stream.Bitrate != 0) ? stream.Bitrate + "kbps " : "") + GetChannelFormatString(stream.NumberOfChannels) + " " + streamer.StreamFormatString + " (" + stream.BitsPerSample + "bit@" + streamer.SamplerateString + ")";
            //TunerScreen.StreamFormat = "";
            //TunerScreen.StreamFormat = streamFormat;
        }

        void _Streamer_OnStreamChanging(object sender, AudioStreamer.StreamChangingEventArgs e)
        {
            TunerScreen.BlinkProgramId = true;
        }

        void _Streamer_OnStreamChanged(object sender, AudioStreamer.StreamChangedEventArgs e)
        {
            AudioStreamer streamer = (AudioStreamer)sender;
            InetStream stream = streamer.CurrentStream;

            TunerScreen.BlinkProgramId = false;
            
            var progId = Memory.GetStation(stream.URL);
            if (progId != null) { TunerScreen.ProgramID = progId.Id; CurrentStationId = progId.Id; }

            TunerScreen.ArtistAndTitle = "";
            TunerScreen.StationName = "";
            TunerScreen.StreamFormat = "";
            TunerScreen.StationName = stream.Station;
            TunerScreen.StreamFormat = ((stream.Bitrate != 0) ? stream.Bitrate + "kbps " : "") + GetChannelFormatString(stream.NumberOfChannels) + " " + streamer.StreamFormatString + " (" + stream.BitsPerSample + "bit@" + streamer.SamplerateString + ")";
            
            if (stream != null) { OperationalState = OperationalStatuses.Play; Director.UpdateInetStatus(true); } else { OperationalState = OperationalStatuses.Stop; }

        }

        void _Streamer_OnStreamEnded(object sender, EventArgs e)
        {
            AudioStreamer streamer = (AudioStreamer)sender;
            InetStream stream = streamer.CurrentStream;

            if (TunerScreen != null)
            {
                TunerScreen.BlinkProgramId = false;
                TunerScreen.StationName = "";
                TunerScreen.StreamFormat = "";
                TunerScreen.ArtistAndTitle = "";
            }

            if (Director  != null)
            {
                // disable stream indicator led - done by UIupdate
                //Director.Display.SetLedBlinker(LedLocations.Bottom, false);
                //Director.Display.SetLedIndicators(LedLocations.Bottom, LedColors.Dark);
            }


            if (OperationalState == OperationalStatuses.Play)
            {

                TunerScreen.ArtistAndTitle = "Stream ended.";
                
                // stream died; this is not good - produce some audio feedback
                Director.Display.ActivatePiezoBuzzer(800, 750);

                if (optReconnectOnBreak.Value)
                {
                    // schedule restart
                    ThreadPool.QueueUserWorkItem(new WaitCallback(ScheduledRestart));
                }

            }

        }


        void ScheduledRestart(object stateInfo)
        {

            // only perform restart if we are still in play mode
            if (OperationalState == OperationalStatuses.Play)
            {

                // get current station
                Station currentStation = Memory.GetStation(_CurrentStationId);

                // increase station connection retry counter
                currentStation.ConnectionRetryCount += 1;

                TunerScreen.ArtistAndTitle = "Reconnecting...";

                try
                {

                    // TODO: Increase timeout with retry attempt numbers
                    // attempt reconnection
                    Streamer.Start(currentStation.Url);
                    
                    // reconnect successful: reset retry counter for this station
                    currentStation.ConnectionRetryCount = 0;

                    // update display
                    TunerScreen.ProgramID = CurrentStationId;
                    TunerScreen.BlinkProgramId = false;


                }
                catch (BASSException bassEx)
                {
                    // reconnect failed
                                        
                    // retried N times already without success?
                    if ((currentStation.ConnectionRetryCount > optReconnectMaxAttempts.Value) && (optReconnectMaxAttempts.Value != 0))
                    {
                        // scan option enabled?
                        if (optScanOnStreamFailure.Value)
                        {
                            currentStation.ConnectionRetryCount = 0;
                            // yes, advance to and try connecting next station
                            Station newStation = Memory.GetNext(CurrentStationId);
                            currentStation = newStation;
                            _CurrentStationId = newStation.Id;
                            TunerScreen.ProgramID = _CurrentStationId;
                            TunerScreen.ArtistAndTitle = "Next Station...";
                            QueueRestart();
                        }
                        else
                        {
                            // give up
                            this.Stop(); // TODO: should we use Streamer.StopAll() instead?
                            TunerScreen.ArtistAndTitle = "Station unavailable.";
                            Director.InternetAvailability.ReportConnectivityProblem();
                        }
                    }
                    else
                    {
                        // reconnect
                        QueueRestart();
                    }
                }
            }

        }

        private void QueueRestart()
        {
            // mandatory delay
            System.Threading.Thread.Sleep(1000);
            // schedule restart
            ThreadPool.QueueUserWorkItem(new WaitCallback(ScheduledRestart));
        }


        // UI update timer
        void UIUpdateTimer_OnElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            // update all physical UI elements (leds, display brightness etc)
            UpdateStreamIndicator();
        }

        // UI update worker function
        void UpdateStreamIndicator()
        {
            if (Streamer.CurrentStream != null)
            {
                // stream present
                if (Streamer.CurrentStream.Status  != InetStream.PlayerStatuses.Stopped)
                {
                    // update stream indicator led
                    int LevelBuffer = Streamer.CurrentStream.BufferLevel;

                    if (LevelBuffer == 0)
                    {
                        // buffer is empty - bad
                        Director.Display.SetLedIndicators(LedLocations.Bottom, LedColors.Red);
                        Director.Display.SetLedBlinker(LedLocations.Bottom, true);
                    }
                    else if (LevelBuffer < 15)
                    {
                        // buffer is low - not good
                        Director.Display.SetLedBlinker(LedLocations.Bottom, false);
                        Director.Display.SetLedIndicators(LedLocations.Bottom, LedColors.Red);
                    }
                    else if ((LevelBuffer >= 15) && (LevelBuffer < 30))
                    {
                        // buffer is moderately filled - okay
                        Director.Display.SetLedBlinker(LedLocations.Bottom, false);
                        Director.Display.SetLedIndicators(LedLocations.Bottom, LedColors.Amber);
                    }
                    else if (LevelBuffer >= 30)
                    {
                        // buffer has at least 3 seconds of audio - good
                        Director.Display.SetLedBlinker(LedLocations.Bottom, false);
                        Director.Display.SetLedIndicators(LedLocations.Bottom, LedColors.Green);
                    }
                }
                else
                {
                    // solid red - stream there but stopped
                    Director.Display.SetLedBlinker(LedLocations.Bottom, false);
                    Director.Display.SetLedIndicators(LedLocations.Bottom, LedColors.Red);
                }
            }
            else
            {
                // stream not present
                // dark led == there is no stream
                Director.Display.SetLedIndicators(LedLocations.Bottom, LedColors.Dark);
            }
        }

        // get channel format name from number of channels
        static string GetChannelFormatString(int channels)
        {
            string fStr;
            switch (channels)
            {
                case 0:
                    fStr = "";
                    break;
                case 1:
                    fStr = "MONO";
                    break;
                case 2:
                    fStr = "STEREO";
                    break;
                case 3:
                    fStr = "2.1";
                    break;
                case 4:
                    fStr = "QUADRO";
                    break;
                case 6:
                    fStr = "5.1";
                    break;
                case 8:
                    fStr = "7.1";
                    break;
                case 9:
                    fStr = "8.1";
                    break;
                default:
                    fStr = channels + "Ch";
                    break;
            }
            return fStr;
        }


        // operational states of the Tuner
        public enum OperationalStatuses
        {
            Stop = 0,
            Play = 1,
            Next,
            Previous
        }

    }
    
}
